Coverage Report

Created: 2026-02-05 09:02

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
D:\a\scloud-dns\scloud-dns\src\config.rs
Line
Count
Source
1
//! Configuration types for scloud-dns
2
//!
3
//! This file contains Serde (Deserialize/Serialize) structs that map to the
4
//! JSON configuration you provided. It includes helpers to load the config
5
//! from a file and a light `validate()` method placeholder you can extend.
6
7
use crate::exceptions::SCloudException;
8
use anyhow::{Context, Result};
9
use serde::{Deserialize, Serialize};
10
use std::collections::HashSet;
11
use std::fs;
12
use std::path::Path;
13
14
/// Top-level configuration
15
#[derive(Debug, Clone, Serialize, Deserialize)]
16
pub struct Config {
17
    #[serde(default)]
18
    pub server: ServerConfig,
19
20
    #[serde(default)]
21
    pub workers: WorkersConfig,
22
23
    #[serde(default)]
24
    pub logging: LoggingConfig,
25
26
    #[serde(default)]
27
    pub metrics: MetricsConfig,
28
29
    #[serde(default)]
30
    pub admin: AdminConfig,
31
32
    #[serde(default)]
33
    pub acl: Vec<AclEntry>,
34
35
    #[serde(default)]
36
    pub listener: Vec<ListenerConfig>,
37
38
    #[serde(default)]
39
    pub doh: DohConfig,
40
41
    #[serde(default)]
42
    pub forwarder: Vec<ForwarderConfig>,
43
44
    #[serde(default)]
45
    pub root_hints: RootHintsConfig,
46
47
    #[serde(default)]
48
    pub cache: CacheConfig,
49
50
    #[serde(default)]
51
    pub recursion: RecursionConfig,
52
53
    #[serde(default)]
54
    pub ratelimit: RateLimitConfig,
55
56
    #[serde(default)]
57
    pub zone: Vec<ZoneConfig>,
58
59
    #[serde(default)]
60
    pub tsig_key: Vec<TsigKey>,
61
62
    #[serde(default)]
63
    pub axfr: AxfrConfig,
64
65
    #[serde(default)]
66
    pub dnssec: DnssecConfig,
67
68
    #[serde(default)]
69
    pub policy: PolicyConfig,
70
71
    #[serde(default)]
72
    pub amplification_mitigation: AmplificationMitigationConfig,
73
74
    #[serde(default)]
75
    pub tuning: TuningConfig,
76
77
    #[serde(default)]
78
    pub view: Vec<ViewConfig>,
79
80
    #[serde(default)]
81
    pub monitoring: MonitoringConfig,
82
83
    #[serde(default)]
84
    pub dynupdate: Vec<DynUpdateConfig>,
85
86
    #[serde(default)]
87
    pub limits: LimitsConfig,
88
}
89
90
impl Config {
91
    /// Load config from a JSON file path
92
9
    pub fn from_file(path: &Path) -> Result<Self, SCloudException> {
93
9
        let s = fs::read_to_string(path)
94
9
            .with_context(|| 
format!0
("reading config file {}",
path0
.
display0
()))
95
9
            .map_err(|_| SCloudException::SCLOUD_CONFIG_FILE_NOT_FOUND)
?0
;
96
9
        let cfg: Config = serde_json::from_str(&s)
97
9
            .context("parsing JSON config")
98
9
            .map_err(|_| SCloudException::SCLOUD_CONFIG_IMPOSSIBLE_TO_PARSE_JSON)
?0
;
99
9
        cfg.validate()
?0
;
100
9
        Ok(cfg)
101
9
    }
102
103
    /// Validation hook
104
10
    pub fn validate(&self) -> Result<(), SCloudException> {
105
24
        let 
acl_names10
:
HashSet<&str>10
=
self.acl.iter()10
.
map10
(|a| a.name.as_str()).
collect10
();
106
16
        let 
tsig_names10
:
HashSet<&str>10
=
self.tsig_key.iter()10
.
map10
(|t| t.name.as_str()).
collect10
();
107
10
        let _forwarder_names: HashSet<&str> =
108
24
            
self.forwarder.iter()10
.
map10
(|f| f.name.as_str()).
collect10
();
109
110
72
        let 
is_acl_ref_valid10
= |s: &str| -> bool {
111
72
            if s.trim().is_empty() {
112
0
                return false;
113
72
            }
114
72
            acl_names.contains(s) || 
s16
.
contains16
('/')
115
72
        };
116
117
10
        if self.server.bind_port == 0 {
118
0
            return Err(SCloudException::SCLOUD_CONFIG_INVALID_SERVER_PORT);
119
10
        }
120
10
        if self.server.max_udp_payload == 0 || self.server.max_udp_payload > 65535 {
121
0
            return Err(SCloudException::SCLOUD_CONFIG_INVALID_MAX_UDP_PAYLOAD);
122
10
        }
123
10
        if self.tuning.max_label_length == 0 || self.tuning.max_label_length > 63 {
124
0
            return Err(SCloudException::SCLOUD_CONFIG_INVALID_DNS_LIMITS);
125
10
        }
126
10
        if self.tuning.max_domain_length == 0 || self.tuning.max_domain_length > 253 {
127
0
            return Err(SCloudException::SCLOUD_CONFIG_INVALID_DNS_LIMITS);
128
10
        }
129
10
        if self.limits.max_udp_packet_size == 0 || self.limits.max_udp_packet_size > 65535 {
130
0
            return Err(SCloudException::SCLOUD_CONFIG_INVALID_DNS_LIMITS);
131
10
        }
132
133
10
        let mut listener_names = HashSet::new();
134
24
        for l in 
&self.listener10
{
135
24
            if l.name.trim().is_empty() {
136
0
                return Err(SCloudException::SCLOUD_CONFIG_INVALID_LISTENER);
137
24
            }
138
24
            if !listener_names.insert(l.name.as_str()) {
139
0
                return Err(SCloudException::SCLOUD_CONFIG_DUPLICATE_LISTENER_NAME);
140
24
            }
141
24
            if l.port == 0 {
142
0
                return Err(SCloudException::SCLOUD_CONFIG_INVALID_LISTENER_PORT);
143
24
            }
144
24
            if l.protocols.is_empty() {
145
0
                return Err(SCloudException::SCLOUD_CONFIG_INVALID_LISTENER_PROTOCOLS);
146
24
            }
147
24
            if !l.acl.trim().is_empty() && !is_acl_ref_valid(&l.acl) {
148
0
                return Err(SCloudException::SCLOUD_CONFIG_UNKNOWN_ACL_REFERENCE);
149
24
            }
150
151
24
            if l.enable_tls.unwrap_or(false) {
152
8
                if l.tls_cert_path.as_deref().unwrap_or("").trim().is_empty() {
153
0
                    return Err(SCloudException::SCLOUD_CONFIG_TLS_MISSING_CERT);
154
8
                }
155
8
                if l.tls_key_path.as_deref().unwrap_or("").trim().is_empty() {
156
0
                    return Err(SCloudException::SCLOUD_CONFIG_TLS_MISSING_KEY);
157
8
                }
158
8
                if !l.protocols.iter().any(|p| matches!(p, Protocol::TCP)) {
159
0
                    return Err(SCloudException::SCLOUD_CONFIG_TLS_REQUIRES_TCP);
160
8
                }
161
16
            }
162
        }
163
164
10
        if self.doh.enabled {
165
8
            if self
166
8
                .doh
167
8
                .tls_cert_path
168
8
                .as_deref()
169
8
                .unwrap_or("")
170
8
                .trim()
171
8
                .is_empty()
172
            {
173
0
                return Err(SCloudException::SCLOUD_CONFIG_TLS_MISSING_CERT);
174
8
            }
175
8
            if self
176
8
                .doh
177
8
                .tls_key_path
178
8
                .as_deref()
179
8
                .unwrap_or("")
180
8
                .trim()
181
8
                .is_empty()
182
            {
183
0
                return Err(SCloudException::SCLOUD_CONFIG_TLS_MISSING_KEY);
184
8
            }
185
8
            if self.doh.paths.is_empty() {
186
0
                return Err(SCloudException::SCLOUD_CONFIG_INVALID_DOH);
187
8
            }
188
2
        }
189
190
10
        if self.recursion.enabled {
191
8
            if self.recursion.allowed_acl.trim().is_empty() {
192
0
                return Err(SCloudException::SCLOUD_CONFIG_UNKNOWN_ACL_REFERENCE);
193
8
            }
194
8
            if !is_acl_ref_valid(&self.recursion.allowed_acl) {
195
0
                return Err(SCloudException::SCLOUD_CONFIG_UNKNOWN_ACL_REFERENCE);
196
8
            }
197
2
        }
198
199
10
        let mut fwd_names = HashSet::new();
200
24
        for f in 
&self.forwarder10
{
201
24
            if f.name.trim().is_empty() {
202
0
                return Err(SCloudException::SCLOUD_CONFIG_INVALID_FORWARDER);
203
24
            }
204
24
            if !fwd_names.insert(f.name.as_str()) {
205
0
                return Err(SCloudException::SCLOUD_CONFIG_DUPLICATE_FORWARDER_NAME);
206
24
            }
207
24
            if f.addresses.is_empty() {
208
0
                return Err(SCloudException::SCLOUD_CONFIG_INVALID_FORWARDER);
209
24
            }
210
40
            for a in 
&f.addresses24
{
211
40
                if a.parse::<std::net::SocketAddr>().is_err() {
212
0
                    return Err(SCloudException::SCLOUD_CONFIG_IMPOSSIBLE_TO_PARSE_ADDR);
213
40
                }
214
            }
215
        }
216
217
10
        let mut zone_names = HashSet::new();
218
32
        for z in 
&self.zone10
{
219
32
            if z.name.trim().is_empty() {
220
0
                return Err(SCloudException::SCLOUD_CONFIG_INVALID_ZONE);
221
32
            }
222
32
            if !zone_names.insert(z.name.as_str()) {
223
0
                return Err(SCloudException::SCLOUD_CONFIG_DUPLICATE_ZONE_NAME);
224
32
            }
225
226
32
            match z.kind {
227
                ZoneType::Master => {
228
16
                    let inline = z.inline.unwrap_or(false);
229
16
                    if inline {
230
8
                        if z.records.is_empty() {
231
0
                            return Err(SCloudException::SCLOUD_CONFIG_INVALID_INLINE_ZONE);
232
8
                        }
233
8
                        let has_soa = z
234
8
                            .records
235
8
                            .iter()
236
8
                            .any(|r| r.r#type.eq_ignore_ascii_case("SOA"));
237
8
                        if !has_soa {
238
0
                            return Err(SCloudException::SCLOUD_CONFIG_INVALID_INLINE_ZONE);
239
8
                        }
240
                    } else {
241
8
                        if z.file.as_deref().unwrap_or("").trim().is_empty() {
242
0
                            return Err(SCloudException::SCLOUD_CONFIG_ZONE_MISSING_FILE);
243
8
                        }
244
                    }
245
246
16
                    if let Some(
acl8
) = z.notify_acl.as_deref() {
247
8
                        if !acl.trim().is_empty() && !is_acl_ref_valid(acl) {
248
0
                            return Err(SCloudException::SCLOUD_CONFIG_UNKNOWN_ACL_REFERENCE);
249
8
                        }
250
8
                    }
251
16
                    if let Some(
acl8
) = z.allow_transfer_acl.as_deref() {
252
8
                        if !acl.trim().is_empty() && !is_acl_ref_valid(acl) {
253
0
                            return Err(SCloudException::SCLOUD_CONFIG_UNKNOWN_ACL_REFERENCE);
254
8
                        }
255
8
                    }
256
257
16
                    if let Some(
k8
) = z.axfr_tsig_key.as_deref() {
258
8
                        if !k.trim().is_empty() && !tsig_names.contains(k) {
259
0
                            return Err(SCloudException::SCLOUD_CONFIG_UNKNOWN_TSIG_KEY);
260
8
                        }
261
8
                    }
262
                }
263
                ZoneType::Slave => {
264
8
                    if z.masters.is_empty() {
265
0
                        return Err(SCloudException::SCLOUD_CONFIG_SLAVE_MISSING_MASTERS);
266
8
                    }
267
8
                    for m in &z.masters {
268
8
                        if m.parse::<std::net::SocketAddr>().is_err() {
269
0
                            return Err(SCloudException::SCLOUD_CONFIG_IMPOSSIBLE_TO_PARSE_ADDR);
270
8
                        }
271
                    }
272
8
                    if z.file.as_deref().unwrap_or("").trim().is_empty() {
273
0
                        return Err(SCloudException::SCLOUD_CONFIG_ZONE_MISSING_FILE);
274
8
                    }
275
                }
276
                ZoneType::Forward => {
277
8
                    if z.forwarders.is_empty() {
278
0
                        return Err(SCloudException::SCLOUD_CONFIG_FORWARD_ZONE_MISSING_FORWARDERS);
279
8
                    }
280
8
                    for f in &z.forwarders {
281
8
                        if f.parse::<std::net::SocketAddr>().is_err() {
282
0
                            return Err(SCloudException::SCLOUD_CONFIG_IMPOSSIBLE_TO_PARSE_ADDR);
283
8
                        }
284
                    }
285
                }
286
0
                ZoneType::Stub => {
287
0
                    // TODO: not defined JSON yet, strict checks later when I will implement it.
288
0
                }
289
            }
290
291
48
            for r in 
&z.records32
{
292
48
                if r.r#type.eq_ignore_ascii_case("MX") {
293
8
                    if r.priority.is_none() {
294
0
                        return Err(SCloudException::SCLOUD_CONFIG_MX_MISSING_PRIORITY);
295
8
                    }
296
40
                } else if r.priority.is_some() {
297
0
                    return Err(SCloudException::SCLOUD_CONFIG_PRIORITY_ON_NON_MX);
298
40
                }
299
            }
300
        }
301
302
10
        let mut view_names = HashSet::new();
303
16
        for v in 
&self.view10
{
304
16
            if v.name.trim().is_empty() {
305
0
                return Err(SCloudException::SCLOUD_CONFIG_INVALID_VIEW);
306
16
            }
307
16
            if !view_names.insert(v.name.as_str()) {
308
0
                return Err(SCloudException::SCLOUD_CONFIG_DUPLICATE_VIEW_NAME);
309
16
            }
310
16
            if v.acl.trim().is_empty() || !is_acl_ref_valid(&v.acl) {
311
0
                return Err(SCloudException::SCLOUD_CONFIG_UNKNOWN_ACL_REFERENCE);
312
16
            }
313
16
            for vz in &v.zones {
314
16
                if vz.name.trim().is_empty() || vz.file.trim().is_empty() {
315
0
                    return Err(SCloudException::SCLOUD_CONFIG_INVALID_VIEW);
316
16
                }
317
            }
318
        }
319
320
10
        for 
d8
in &self.dynupdate {
321
8
            if d.zone.trim().is_empty() {
322
0
                return Err(SCloudException::SCLOUD_CONFIG_INVALID_DYNUPDATE);
323
8
            }
324
8
            if d.acl.trim().is_empty() || !is_acl_ref_valid(&d.acl) {
325
0
                return Err(SCloudException::SCLOUD_CONFIG_UNKNOWN_ACL_REFERENCE);
326
8
            }
327
8
            if let Some(k) = d.tsig_key.as_deref() {
328
8
                if !k.trim().is_empty() && !tsig_names.contains(k) {
329
0
                    return Err(SCloudException::SCLOUD_CONFIG_UNKNOWN_TSIG_KEY);
330
8
                }
331
0
            }
332
333
8
            if !zone_names.contains(d.zone.as_str()) {
334
0
                return Err(SCloudException::SCLOUD_CONFIG_DYNUPDATE_UNKNOWN_ZONE);
335
8
            }
336
        }
337
338
10
        Ok(())
339
10
    }
340
341
    /// Get the address of a specific forwarder by index value
342
    #[allow(unused)]
343
5
    pub(crate) fn try_get_forwarder_addr_by_index(
344
5
        &self,
345
5
        forwarder_index: usize,
346
5
        address_index: usize,
347
5
    ) -> Result<std::net::SocketAddr, SCloudException> {
348
5
        let addr = self
349
5
            .forwarder
350
5
            .get(forwarder_index)
351
5
            .ok_or(SCloudException::SCLOUD_CONFIG_MISSING_FORWARDER)
?0
352
            .addresses
353
5
            .get(address_index)
354
5
            .ok_or(SCloudException::SCLOUD_CONFIG_MISSING_ADDRESS)
?0
355
5
            .parse()
356
5
            .map_err(|_| SCloudException::SCLOUD_CONFIG_IMPOSSIBLE_TO_PARSE_ADDR)
?0
;
357
358
5
        Ok(addr)
359
5
    }
360
361
    // TODO: add a loop to test the next address for each retry
362
5
    pub(crate) fn try_get_forwarder_addr_by_name(
363
5
        &self,
364
5
        forwarder_name: &str,
365
5
    ) -> Result<std::net::SocketAddr, SCloudException> {
366
5
        let forwarder = self
367
5
            .forwarder
368
5
            .iter()
369
12
            .
find5
(|f| f.name == forwarder_name)
370
5
            .ok_or(SCloudException::SCLOUD_CONFIG_MISSING_FORWARDER)
?0
;
371
372
5
        for addr_str in &forwarder.addresses {
373
5
            if let Ok(addr) = addr_str.parse::<std::net::SocketAddr>() {
374
5
                return Ok(addr);
375
0
            }
376
        }
377
378
0
        Err(SCloudException::SCLOUD_CONFIG_IMPOSSIBLE_TO_PARSE_ADDR)
379
5
    }
380
}
381
382
impl Default for Config {
383
4
    fn default() -> Self {
384
4
        Self {
385
4
            server: ServerConfig::default(),
386
4
            workers: WorkersConfig::default(),
387
4
            logging: LoggingConfig::default(),
388
4
            metrics: MetricsConfig::default(),
389
4
            admin: AdminConfig::default(),
390
4
            acl: Vec::new(),
391
4
            listener: Vec::new(),
392
4
            doh: DohConfig::default(),
393
4
            forwarder: Vec::new(),
394
4
            root_hints: RootHintsConfig::default(),
395
4
            cache: CacheConfig::default(),
396
4
            recursion: RecursionConfig::default(),
397
4
            ratelimit: RateLimitConfig::default(),
398
4
            zone: Vec::new(),
399
4
            tsig_key: Vec::new(),
400
4
            axfr: AxfrConfig::default(),
401
4
            dnssec: DnssecConfig::default(),
402
4
            policy: PolicyConfig::default(),
403
4
            amplification_mitigation: AmplificationMitigationConfig::default(),
404
4
            tuning: TuningConfig::default(),
405
4
            view: Vec::new(),
406
4
            monitoring: MonitoringConfig::default(),
407
4
            dynupdate: Vec::new(),
408
4
            limits: LimitsConfig::default(),
409
4
        }
410
4
    }
411
}
412
413
#[derive(Debug, Clone, Serialize, Deserialize)]
414
pub struct ServerConfig {
415
    pub name: String,
416
    pub environment: String,
417
    pub max_concurrent_requests: usize,
418
    pub graceful_shutdown_timeout_secs: u64,
419
420
    pub default_ttl: u32,
421
    pub max_udp_payload: usize,
422
    pub enable_edns: bool,
423
    pub enable_tcp: bool,
424
    pub enable_dnssec: bool,
425
426
    pub bind_port: u16,
427
}
428
429
impl Default for ServerConfig {
430
5
    fn default() -> Self {
431
5
        ServerConfig {
432
5
            name: "scloud-dns".to_string(),
433
5
            environment: "production".to_string(),
434
5
            max_concurrent_requests: 5000,
435
5
            graceful_shutdown_timeout_secs: 15,
436
5
            default_ttl: 3600,
437
5
            max_udp_payload: 4096,
438
5
            enable_edns: true,
439
5
            enable_tcp: true,
440
5
            enable_dnssec: false,
441
5
            bind_port: 53,
442
5
        }
443
5
    }
444
}
445
446
#[derive(Debug, Clone, Serialize, Deserialize)]
447
pub struct WorkersConfig {
448
    pub listener: u16,
449
    pub decoder: u16,
450
    pub query_dispatcher: u16,
451
    pub cache_lookup: u16,
452
    pub zone_manager: u16,
453
    pub resolver: u16,
454
    pub cache_writer: u16,
455
    pub encoder: u16,
456
    pub sender: u16,
457
    pub cache_janitor: u16,
458
    pub metrics: u16,
459
    pub tcp_acceptor: u16,
460
}
461
462
impl Default for WorkersConfig {
463
4
    fn default() -> Self {
464
4
        WorkersConfig {
465
4
            listener: 5,
466
4
            decoder: 5,
467
4
            query_dispatcher: 3,
468
4
            cache_lookup: 3,
469
4
            zone_manager: 1,
470
4
            resolver: 5,
471
4
            cache_writer: 1,
472
4
            encoder: 5,
473
4
            sender: 5,
474
4
            cache_janitor: 1,
475
4
            metrics: 2,
476
4
            tcp_acceptor: 1,
477
4
        }
478
4
    }
479
}
480
481
#[derive(Debug, Clone, Serialize, Deserialize)]
482
pub struct LoggingConfig {
483
    pub level: LogLevel,
484
    pub format: LogFormat,
485
    pub file: String,
486
    pub rotate: bool,
487
    pub live_print: bool,
488
    pub max_size_mb: u64,
489
}
490
491
impl Default for LoggingConfig {
492
4
    fn default() -> Self {
493
4
        LoggingConfig {
494
4
            level: LogLevel::INFO,
495
4
            format: LogFormat::TEXT,
496
4
            file: "/var/log/scloud-dns/scloud-dns.log".to_string(),
497
4
            rotate: true,
498
4
            live_print: false,
499
4
            max_size_mb: 200,
500
4
        }
501
4
    }
502
}
503
504
#[allow(non_camel_case_types)]
505
#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
506
#[serde(rename_all = "lowercase")]
507
pub enum LogLevel {
508
    TRACE = 0,
509
    DEBUG = 1,
510
    INFO = 2,
511
    WARN = 3,
512
    ERROR = 4,
513
    FATAL = 5,
514
}
515
516
impl LogLevel {
517
0
    pub fn parse(s: &str) -> Self {
518
0
        match s.to_ascii_lowercase().as_str() {
519
0
            "trace" => Self::TRACE,
520
0
            "debug" => Self::DEBUG,
521
0
            "info" => Self::INFO,
522
0
            "warn" | "warning" => Self::WARN,
523
0
            "error" => Self::ERROR,
524
0
            "fatal" => Self::FATAL,
525
0
            _ => Self::WARN,
526
        }
527
0
    }
528
529
0
    pub(crate) fn as_str(self) -> &'static str {
530
0
        match self {
531
0
            Self::TRACE => "trace",
532
0
            Self::DEBUG => "debug",
533
0
            Self::INFO => "info",
534
0
            Self::WARN => "warn",
535
0
            Self::ERROR => "error",
536
0
            Self::FATAL => "fatal",
537
        }
538
0
    }
539
}
540
541
#[allow(non_camel_case_types)]
542
#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq)]
543
#[serde(rename_all = "lowercase")]
544
pub enum LogFormat {
545
    JSON,
546
    TEXT,
547
}
548
549
impl LogFormat {
550
0
    pub fn parse(s: &str) -> Self {
551
0
        match s.to_ascii_lowercase().as_str() {
552
0
            "json" => Self::JSON,
553
0
            _ => Self::TEXT,
554
        }
555
0
    }
556
}
557
558
#[derive(Debug, Clone, Serialize, Deserialize)]
559
pub struct MetricsConfig {
560
    pub enabled: bool,
561
    pub prometheus_bind: String,
562
    pub enable_health_endpoint: bool,
563
    pub health_bind: String,
564
}
565
566
impl Default for MetricsConfig {
567
4
    fn default() -> Self {
568
4
        MetricsConfig {
569
4
            enabled: true,
570
4
            prometheus_bind: "0.0.0.0:9153".to_string(),
571
4
            enable_health_endpoint: true,
572
4
            health_bind: "127.0.0.1:8081".to_string(),
573
4
        }
574
4
    }
575
}
576
577
#[derive(Debug, Clone, Serialize, Deserialize)]
578
pub struct AdminConfig {
579
    pub enabled: bool,
580
    pub bind: String,
581
    pub auth_token: String,
582
    pub enable_tls: bool,
583
}
584
585
impl Default for AdminConfig {
586
4
    fn default() -> Self {
587
4
        AdminConfig {
588
4
            enabled: true,
589
4
            bind: "127.0.0.1:8053".to_string(),
590
4
            auth_token: "replace-with-secure-token".to_string(),
591
4
            enable_tls: false,
592
4
        }
593
4
    }
594
}
595
596
#[derive(Debug, Clone, Serialize, Deserialize)]
597
pub struct AclEntry {
598
    pub name: String,
599
    pub networks: Vec<String>, // CIDRs or single IPs; parse later with ipnet or similar
600
}
601
602
#[derive(Debug, Clone, Serialize, Deserialize)]
603
pub struct ListenerConfig {
604
    pub name: String,
605
    pub address: String,
606
    pub port: u16,
607
    #[serde(default)]
608
    pub protocols: Vec<Protocol>,
609
    #[serde(default)]
610
    pub recursion_allowed: bool,
611
    /// ACL name or a raw CIDR/list string
612
    #[serde(default)]
613
    pub acl: String,
614
    #[serde(default)]
615
    pub workers: Option<usize>,
616
    #[serde(default)]
617
    pub enable_tls: Option<bool>,
618
    #[serde(default)]
619
    pub tls_cert_path: Option<String>,
620
    #[serde(default)]
621
    pub tls_key_path: Option<String>,
622
}
623
624
impl Default for ListenerConfig {
625
1
    fn default() -> Self {
626
1
        ListenerConfig {
627
1
            name: String::new(),
628
1
            address: "0.0.0.0".to_string(),
629
1
            port: 53,
630
1
            protocols: vec![Protocol::UDP],
631
1
            recursion_allowed: false,
632
1
            acl: "0.0.0.0/0".to_string(),
633
1
            workers: None,
634
1
            enable_tls: None,
635
1
            tls_cert_path: None,
636
1
            tls_key_path: None,
637
1
        }
638
1
    }
639
}
640
641
#[derive(Debug, Clone, Serialize, Deserialize)]
642
#[serde(rename_all = "lowercase")]
643
pub enum Protocol {
644
    UDP,
645
    TCP,
646
}
647
648
#[derive(Debug, Clone, Serialize, Deserialize)]
649
pub struct DohConfig {
650
    pub enabled: bool,
651
    pub bind: String,
652
    #[serde(default)]
653
    pub tls_cert_path: Option<String>,
654
    #[serde(default)]
655
    pub tls_key_path: Option<String>,
656
    #[serde(default)]
657
    pub paths: Vec<String>,
658
    #[serde(default)]
659
    pub allowed_origins: Vec<String>,
660
}
661
662
impl Default for DohConfig {
663
5
    fn default() -> Self {
664
5
        DohConfig {
665
5
            enabled: false,
666
5
            bind: "0.0.0.0:443".to_string(),
667
5
            tls_cert_path: None,
668
5
            tls_key_path: None,
669
5
            paths: vec!["/dns-query".to_string()],
670
5
            allowed_origins: Vec::new(),
671
5
        }
672
5
    }
673
}
674
675
#[derive(Debug, Clone, Serialize, Deserialize)]
676
pub struct ForwarderConfig {
677
    pub name: String,
678
    pub addresses: Vec<String>,
679
    pub policy: ForwardPolicy,
680
    pub timeout_ms: u64,
681
    pub edns: bool,
682
    pub use_tcp_on_retry: Option<bool>,
683
}
684
685
impl Default for ForwarderConfig {
686
1
    fn default() -> Self {
687
1
        ForwarderConfig {
688
1
            name: String::new(),
689
1
            addresses: Vec::new(),
690
1
            policy: ForwardPolicy::First,
691
1
            timeout_ms: 1500,
692
1
            edns: true,
693
1
            use_tcp_on_retry: Some(true),
694
1
        }
695
1
    }
696
}
697
698
#[derive(Debug, Clone, Serialize, Deserialize)]
699
#[serde(rename_all = "snake_case")]
700
#[derive(PartialEq)]
701
pub enum ForwardPolicy {
702
    RoundRobin,
703
    First,
704
    Random,
705
}
706
707
#[derive(Debug, Clone, Serialize, Deserialize)]
708
pub struct RootHintsConfig {
709
    pub file: String,
710
}
711
712
impl Default for RootHintsConfig {
713
4
    fn default() -> Self {
714
4
        RootHintsConfig {
715
4
            file: "/etc/scloud/root.hints".to_string(),
716
4
        }
717
4
    }
718
}
719
720
#[derive(Debug, Clone, Serialize, Deserialize)]
721
pub struct CacheConfig {
722
    pub enabled: bool,
723
    pub max_entries: usize,
724
    pub max_ttl_seconds: u64,
725
    pub negative_ttl_seconds: u64,
726
    pub eviction_policy: String,
727
}
728
729
impl Default for CacheConfig {
730
5
    fn default() -> Self {
731
5
        CacheConfig {
732
5
            enabled: true,
733
5
            max_entries: 200_000,
734
5
            max_ttl_seconds: 86_400,
735
5
            negative_ttl_seconds: 300,
736
5
            eviction_policy: "lru".to_string(),
737
5
        }
738
5
    }
739
}
740
741
#[derive(Debug, Clone, Serialize, Deserialize)]
742
pub struct RecursionConfig {
743
    pub enabled: bool,
744
    pub allowed_acl: String,
745
    pub max_recursive_queries: usize,
746
    pub recursion_timeout_ms: u64,
747
    pub retry_interval_ms: u64,
748
}
749
750
impl Default for RecursionConfig {
751
5
    fn default() -> Self {
752
5
        RecursionConfig {
753
5
            enabled: false,
754
5
            allowed_acl: "internal".to_string(),
755
5
            max_recursive_queries: 50,
756
5
            recursion_timeout_ms: 5000,
757
5
            retry_interval_ms: 200,
758
5
        }
759
5
    }
760
}
761
762
#[derive(Debug, Clone, Serialize, Deserialize)]
763
pub struct RateLimitConfig {
764
    pub enabled: bool,
765
    pub global_qps: u64,
766
    pub per_ip_qps: u64,
767
    pub per_subnet_qps: u64,
768
    pub rrl: RrlConfig,
769
}
770
771
impl Default for RateLimitConfig {
772
5
    fn default() -> Self {
773
5
        RateLimitConfig {
774
5
            enabled: true,
775
5
            global_qps: 3000,
776
5
            per_ip_qps: 100,
777
5
            per_subnet_qps: 1000,
778
5
            rrl: RrlConfig::default(),
779
5
        }
780
5
    }
781
}
782
783
#[derive(Debug, Clone, Serialize, Deserialize)]
784
pub struct RrlConfig {
785
    pub enabled: bool,
786
    pub window_seconds: u64,
787
    pub slip: u32,
788
    pub qps_threshold: u64,
789
}
790
791
impl Default for RrlConfig {
792
5
    fn default() -> Self {
793
5
        RrlConfig {
794
5
            enabled: true,
795
5
            window_seconds: 5,
796
5
            slip: 2,
797
5
            qps_threshold: 50,
798
5
        }
799
5
    }
800
}
801
802
#[derive(Debug, Clone, Serialize, Deserialize)]
803
pub struct ZoneConfig {
804
    pub name: String,
805
    #[serde(rename = "type")]
806
    pub kind: ZoneType,
807
    #[serde(default)]
808
    pub file: Option<String>,
809
    #[serde(default)]
810
    pub notify: Option<bool>,
811
    #[serde(default)]
812
    pub notify_acl: Option<String>,
813
    #[serde(default)]
814
    pub allow_transfer_acl: Option<String>,
815
    #[serde(default)]
816
    pub allow_update_acl: Option<String>,
817
    #[serde(default)]
818
    pub axfr_tsig_key: Option<String>,
819
820
    // Slave-specific
821
    #[serde(default)]
822
    pub masters: Vec<String>,
823
824
    // Inline zone
825
    #[serde(default)]
826
    pub inline: Option<bool>,
827
    #[serde(default)]
828
    pub records: Vec<ZoneRecord>,
829
830
    // Forward-specific
831
    #[serde(default)]
832
    pub forwarders: Vec<String>,
833
    #[serde(default)]
834
    pub forward_policy: Option<String>,
835
}
836
837
impl Default for ZoneConfig {
838
1
    fn default() -> Self {
839
1
        ZoneConfig {
840
1
            name: String::new(),
841
1
            kind: ZoneType::Master,
842
1
            file: None,
843
1
            notify: Some(false),
844
1
            notify_acl: None,
845
1
            allow_transfer_acl: None,
846
1
            allow_update_acl: None,
847
1
            axfr_tsig_key: None,
848
1
            masters: Vec::new(),
849
1
            inline: Some(false),
850
1
            records: Vec::new(),
851
1
            forwarders: Vec::new(),
852
1
            forward_policy: None,
853
1
        }
854
1
    }
855
}
856
857
#[derive(Debug, Clone, Serialize, Deserialize)]
858
#[serde(rename_all = "lowercase")]
859
#[derive(PartialEq)]
860
pub enum ZoneType {
861
    Master,
862
    Slave,
863
    Forward,
864
    Stub,
865
}
866
867
#[derive(Debug, Clone, Serialize, Deserialize)]
868
pub struct ZoneRecord {
869
    pub name: String,
870
    pub ttl: Option<u32>,
871
    pub class: Option<String>,
872
    #[serde(rename = "type")]
873
    pub r#type: String,
874
    pub rdata: String,
875
    #[serde(default)]
876
    pub priority: Option<u16>,
877
}
878
879
#[derive(Debug, Clone, Serialize, Deserialize)]
880
pub struct TsigKey {
881
    pub name: String,
882
    pub algorithm: String,
883
    pub secret: String, // TODO: base64 encoded - do not keep in plaintext in production
884
}
885
886
#[derive(Debug, Clone, Serialize, Deserialize)]
887
pub struct AxfrConfig {
888
    pub enabled: bool,
889
    pub max_concurrent_transfers: usize,
890
    pub transfer_timeout_secs: u64,
891
}
892
893
impl Default for AxfrConfig {
894
5
    fn default() -> Self {
895
5
        AxfrConfig {
896
5
            enabled: true,
897
5
            max_concurrent_transfers: 4,
898
5
            transfer_timeout_secs: 120,
899
5
        }
900
5
    }
901
}
902
903
#[derive(Debug, Clone, Serialize, Deserialize)]
904
pub struct DnssecConfig {
905
    pub enabled: bool,
906
    pub auto_sign: bool,
907
    pub default_algo: String,
908
    pub kasp_file: Option<String>,
909
}
910
911
impl Default for DnssecConfig {
912
5
    fn default() -> Self {
913
5
        DnssecConfig {
914
5
            enabled: false,
915
5
            auto_sign: false,
916
5
            default_algo: "RSASHA256".to_string(),
917
5
            kasp_file: None,
918
5
        }
919
5
    }
920
}
921
922
#[derive(Debug, Clone, Serialize, Deserialize)]
923
pub struct PolicyConfig {
924
    #[serde(default)]
925
    pub deny_domains: Vec<String>,
926
}
927
928
impl Default for PolicyConfig {
929
4
    fn default() -> Self {
930
4
        PolicyConfig {
931
4
            deny_domains: Vec::new(),
932
4
        }
933
4
    }
934
}
935
936
#[derive(Debug, Clone, Serialize, Deserialize)]
937
pub struct AmplificationMitigationConfig {
938
    pub drop_fragments: bool,
939
    pub max_response_size_udp: usize,
940
}
941
942
impl Default for AmplificationMitigationConfig {
943
4
    fn default() -> Self {
944
4
        AmplificationMitigationConfig {
945
4
            drop_fragments: true,
946
4
            max_response_size_udp: 4096,
947
4
        }
948
4
    }
949
}
950
951
#[derive(Debug, Clone, Serialize, Deserialize)]
952
pub struct TuningConfig {
953
    pub socket_recv_buffer_bytes: usize,
954
    pub socket_send_buffer_bytes: usize,
955
    pub max_label_length: usize,
956
    pub max_domain_length: usize,
957
}
958
959
impl Default for TuningConfig {
960
4
    fn default() -> Self {
961
4
        TuningConfig {
962
4
            socket_recv_buffer_bytes: 262_144,
963
4
            socket_send_buffer_bytes: 262_144,
964
4
            max_label_length: 63,
965
4
            max_domain_length: 253,
966
4
        }
967
4
    }
968
}
969
970
#[derive(Debug, Clone, Serialize, Deserialize)]
971
pub struct ViewConfig {
972
    pub name: String,
973
    pub acl: String,
974
    #[serde(default)]
975
    pub zones: Vec<ViewZone>,
976
}
977
978
#[derive(Debug, Clone, Serialize, Deserialize)]
979
pub struct ViewZone {
980
    pub name: String,
981
    pub file: String,
982
}
983
984
#[derive(Debug, Clone, Serialize, Deserialize)]
985
pub struct MonitoringConfig {
986
    pub enable_query_logging: bool,
987
    pub query_log_path: String,
988
    pub log_query_qps: u64,
989
}
990
991
impl Default for MonitoringConfig {
992
4
    fn default() -> Self {
993
4
        MonitoringConfig {
994
4
            enable_query_logging: false,
995
4
            query_log_path: "/var/log/scloud-dns/queries.log".to_string(),
996
4
            log_query_qps: 1000,
997
4
        }
998
4
    }
999
}
1000
1001
#[derive(Debug, Clone, Serialize, Deserialize)]
1002
pub struct DynUpdateConfig {
1003
    pub zone: String,
1004
    pub acl: String,
1005
    pub tsig_key: Option<String>,
1006
    pub allow: bool,
1007
}
1008
1009
#[derive(Debug, Clone, Serialize, Deserialize)]
1010
pub struct LimitsConfig {
1011
    pub max_udp_packet_size: usize,
1012
    pub max_queries_per_minute_per_ip: u64,
1013
    pub max_tcp_sessions_per_ip: usize,
1014
}
1015
1016
impl Default for LimitsConfig {
1017
5
    fn default() -> Self {
1018
5
        LimitsConfig {
1019
5
            max_udp_packet_size: 4096,
1020
5
            max_queries_per_minute_per_ip: 1000,
1021
5
            max_tcp_sessions_per_ip: 8,
1022
5
        }
1023
5
    }
1024
}